// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: xguo

#pragma once

#include <chrono>
#include <cstdint>
#include <functional>
#include <iostream>
#include <string>

#include <boost/date_time/posix_time/posix_time.hpp>

#include "coin2/base/date_util.h"
#include "coin2/exchange/base/market/enums.h"
#include "coin2/exchange/base/symbology/symbol.h"
#include "coin2/exchange/base/symbology/util.h"
#include "coin2/exchange/symbology.pb.h"

namespace coin2::exchange::base::symbology {

using ContractType = coin::proto::ContractType;
using OptionsSide = coin::proto::OptionsSide;

inline const std::string& ContractTypeToString(const ContractType type) {
  return ContractType_Name(type);
}

inline std::optional<ContractType> StringToContractType(const std::string& contract_type_str) {
  ContractType contract_type;
  bool success = coin::proto::ContractType_Parse(contract_type_str, &contract_type);
  if (success) {
    return contract_type;
  } else {
    return {};
  }
}

inline std::string OptionsSideToString(const OptionsSide side) {
  switch (side) {
    case OptionsSide::OPTIONS_SIDE_CALL:
      return "C";
    case OptionsSide::OPTIONS_SIDE_PUT:
      return "P";
    default:
      return "UNKNOWN";
  }
}

inline std::optional<OptionsSide> StringToOptionsSide(const std::string& side_str) {
  if (side_str == "C") {
    return OptionsSide::OPTIONS_SIDE_CALL;
  } else if (side_str == "P") {
    return OptionsSide::OPTIONS_SIDE_PUT;
  } else {
    return {};
  }
}

inline std::optional<coin::proto::OptionsType> StringToOptionsType(const std::string& type_str) {
  if (type_str == "AMERICAN") {
    return coin::proto::OptionsType::AMERICAN_OPTIONS;
  } else if (type_str == "EUROPEAN") {
    return coin::proto::OptionsType::EUROPEAN_OPTIONS;
  } else {
    return {};
  }
}

inline int64_t FindExpiryThisDay(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  using namespace boost::posix_time;
  using namespace boost::gregorian;

  const auto current_dt = from_time_t(::ceil(resolve_timestamp / 1e9));
  const auto td = microseconds(expiry_time_of_day_nanosec / 1000);

  if (current_dt.time_of_day().total_nanoseconds() < expiry_time_of_day_nanosec) {
    return DatetimeToTimestamp(ptime(current_dt.date(), td));
  } else {
    return FindExpiryThisDay(DatetimeToTimestamp(
        ptime(current_dt.date(), microseconds(0))) + 25 * 3600 * 1e9, expiry_time_of_day_nanosec);
  }
}

inline int64_t FindExpiryNextDay(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryThisDay(resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryThisDay(ts + 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryThisFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  using namespace boost::posix_time;
  using namespace boost::gregorian;

  const auto current_dt = from_time_t(::ceil(resolve_timestamp / 1e9));
  const auto td = microseconds(expiry_time_of_day_nanosec / 1000);

  if (current_dt.date().day_of_week() == Friday &&
      current_dt.time_of_day().total_nanoseconds() < expiry_time_of_day_nanosec) {
    // Friday just before expiration.
    return DatetimeToTimestamp(ptime(current_dt.date(), td));
  }

  day_iterator day_it(current_dt.date(), 1);
  for (int n = 1; n <= 7; n++) {
    auto dt = *(++day_it);
    if (dt.day_of_week() == Friday) {
      return DatetimeToTimestamp(ptime(dt, td));
    }
  }

  NOTREACHED() << "Failed to found this week friday expiry!";
  return 0;
}

inline int64_t FindExpiryNextFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryThisFriday(resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryThisFriday(ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryNNextFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryNextFriday(resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryThisFriday(ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryNNNextFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryNNextFriday(resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryThisFriday(ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryQuarterlyLastFriday(
    const ExchangeType exchange_type,
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  using namespace boost::posix_time;
  using namespace boost::gregorian;

  const auto current_dt = from_time_t(::ceil(resolve_timestamp / 1e9));
  const auto this_year = static_cast<int>(current_dt.date().year());
  const auto td = microseconds(expiry_time_of_day_nanosec / 1000);

  for (auto year : {this_year, this_year + 1}) {
    for (auto month : {Mar, Jun, Sep, Dec}) {
      auto dt = last_day_of_the_week_in_month(Friday, month).get_date(year);
      auto datetime = ptime(dt, td);

      if (exchange_type == ExchangeType::Okex ||
          exchange_type == ExchangeType::Huobi) {
        if ((datetime - current_dt).total_nanoseconds() > 14 * 24 * 3600 * 1e9) {
          return DatetimeToTimestamp(datetime);
        }
      } else if (exchange_type == ExchangeType::Binance) {
        // 2020-09-25 is the first expiry.
        // otherwise it will find weird expiry, such as 2020-06-26
        boost::posix_time::ptime first_expiry(
            boost::gregorian::date(2020, 9, 25));
        if (datetime >= first_expiry &&
            (datetime - current_dt).total_nanoseconds() > 0) {
          return DatetimeToTimestamp(datetime);
        }
      } else {
        if ((datetime - current_dt).total_nanoseconds() > 0) {
          return DatetimeToTimestamp(datetime);
        }
      }
    }
  }

  NOTREACHED() << "Failed to find quarterly expiry: "
      << ExchangeType_Name(exchange_type) << " "
      << resolve_timestamp << " "
      << expiry_time_of_day_nanosec;
  return 0;
}

inline int64_t FindExpiryNextQuarterlyLastFriday(
    const ExchangeType exchange_type,
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryQuarterlyLastFriday(
      exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryQuarterlyLastFriday(
      exchange_type, ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryNNextQuarterlyLastFriday(
    const ExchangeType exchange_type,
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryNextQuarterlyLastFriday(
      exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryQuarterlyLastFriday(
      exchange_type, ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryNNNextQuarterlyLastFriday(
    const ExchangeType exchange_type,
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryNNextQuarterlyLastFriday(
      exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryQuarterlyLastFriday(
      exchange_type, ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryMonthlyLastFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  using namespace boost::posix_time;
  using namespace boost::gregorian;

  const auto current_dt = from_time_t(::ceil(resolve_timestamp / 1e9));
  const auto this_year = static_cast<int>(current_dt.date().year());
  const auto this_month = static_cast<int>(current_dt.date().month());
  const auto td = microseconds(expiry_time_of_day_nanosec / 1000);

  auto expiry_dt = last_day_of_the_week_in_month(Friday, this_month).get_date(this_year);
  auto expiry_datetime = ptime(expiry_dt, td);

  if ((expiry_datetime - current_dt).total_nanoseconds() < 0) {
    if (this_month == Dec) {
      expiry_dt = last_day_of_the_week_in_month(Friday, Jan).get_date(this_year + 1);
    } else {
      expiry_dt = last_day_of_the_week_in_month(Friday, this_month + 1).get_date(this_year);
    }
    expiry_datetime = ptime(expiry_dt, td);
  }
  return DatetimeToTimestamp(expiry_datetime);
}

inline int64_t FindExpiryNextMonthlyLastFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryMonthlyLastFriday(
      resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryMonthlyLastFriday(
      ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryNNextMonthlyLastFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryNextMonthlyLastFriday(
      resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryMonthlyLastFriday(
      ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryNNNextMonthlyLastFriday(
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  auto ts = FindExpiryNNextMonthlyLastFriday(
      resolve_timestamp, expiry_time_of_day_nanosec);
  return FindExpiryMonthlyLastFriday(
      ts + 24 * 3600 * 1e9, expiry_time_of_day_nanosec);
}

inline int64_t FindExpiryFriday(
    const ExchangeType exchange_type,
    const base::symbology::ContractType contract_type,
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  using base::symbology::ContractType;
  switch (contract_type) {
    case ContractType::WEEK: {
      return FindExpiryThisFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NEXT_WEEK: {
      return FindExpiryNextFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NNEXT_WEEK: {
      return FindExpiryNNextFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NNNEXT_WEEK: {
      return FindExpiryNNNextFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::MONTH: {
      return FindExpiryMonthlyLastFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NEXT_MONTH: {
      return FindExpiryNextMonthlyLastFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NNEXT_MONTH: {
      return FindExpiryNNextMonthlyLastFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NNNEXT_MONTH: {
      return FindExpiryNNNextMonthlyLastFriday(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::QUARTER: {
      return FindExpiryQuarterlyLastFriday(
          exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NEXT_QUARTER: {
      return FindExpiryNextQuarterlyLastFriday(
          exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NNEXT_QUARTER: {
      return FindExpiryNNextQuarterlyLastFriday(
          exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NNNEXT_QUARTER: {
      return FindExpiryNNNextQuarterlyLastFriday(
          exchange_type, resolve_timestamp, expiry_time_of_day_nanosec);
    }
    default: {
      NOTREACHED();
      return 0;
    }
  }
}

inline int64_t FindExpiryDaily(
    const ExchangeType exchange_type,
    const base::symbology::ContractType contract_type,
    const int64_t resolve_timestamp,
    const int64_t expiry_time_of_day_nanosec) {
  using base::symbology::ContractType;
  switch (contract_type) {
    case ContractType::DAY: {
      return FindExpiryThisDay(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    case ContractType::NEXT_DAY: {
      return FindExpiryNextDay(resolve_timestamp, expiry_time_of_day_nanosec);
    }
    default: {
      NOTREACHED();
      return 0;
    }
  }
}
}  // namespace coin2::exchange::base::symbology
